﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using FastColoredTextBoxNS;
using System.Drawing.Drawing2D;
using System.Text.RegularExpressions;

namespace JETI {
    public partial class Editor: UserControl {
        public Editor( ) {
            InitializeComponent( );
        }

        enum eEngineExec {
            Run,
            Stop,
            Step,
            Continue,
            Breakpoint
        }

        enum eEngineFlow {
            Stopped,
            Running,
            Stepping,
            Breakpointing
        }

        eEngineExec engineStatus = eEngineExec.Stop;
        eEngineFlow engineFlow = eEngineFlow.Stopped;
        int CurrentExecutionLine = -1;
        Jint.JintEngine engine;

        string[ ] keywords = { "assert", "debugger", "alert", "write", "break", "continue", "do", "for", "import", "new", "this", "void", "case", "default", "else", "function", "in", "return", "typeof", "while", "comment", "delete", "export", "if", "label", "switch", "var", "with" };

        Style CommentStyle = new TextStyle( Brushes.Green, null, FontStyle.Regular );
        Style KeywordsStyle = new TextStyle( Brushes.DarkBlue, null, FontStyle.Regular );
        Style FunctionsStyle = new TextStyle( Brushes.DarkBlue, null, FontStyle.Bold );
        Style TextStyle = new TextStyle( Brushes.DarkRed, null, FontStyle.Regular );
        Style NumberStyle = new TextStyle( Brushes.Blue, null, FontStyle.Regular );

        // Key: Line.UniqueId
        HashSet<int> BreakpointsLineId = new HashSet<int>( );
        // Index - Breakpoint Number, Value - Line.UniqueId
        List<int> Breakpoints = new List<int>( );

        private void Editor_Load( object sender, EventArgs e ) {

            txtScript.KeyUp += new KeyEventHandler( txtScript_KeyUp );
            txtScript.TextChanged += new EventHandler<TextChangedEventArgs>( txtScript_TextChanged );
            txtScript.PaintLine += new EventHandler<PaintLineEventArgs>( txtScript_PaintLine );
            txtScript.LineRemoved += new EventHandler<LineRemovedEventArgs>( txtScript_LineRemoved );

            txtScript.AddStyle( KeywordsStyle );
            txtScript.AddStyle( FunctionsStyle );
            txtScript.AddStyle( CommentStyle );
            txtScript.AddStyle( TextStyle );
            txtScript.AddStyle( NumberStyle );

            txtScript.DefaultStyle.ForeBrush = Brushes.Black;

            SetUpEngine( );

        }

        void SetUpEngine( ) {

            if ( engine != null ) {
                engine.Step -= new EventHandler<Jint.Debugger.DebugInformation>( engine_Step );
            }

            engine = new Jint.JintEngine( );
            engine.AllowClr = true;
            engine.DisableSecurity( );

            engine.Step += new EventHandler<Jint.Debugger.DebugInformation>( engine_Step );

            engine.SetFunction( "alert", new Func<string, DialogResult>( o => {
                return MessageBox.Show( o, "Script Alert", MessageBoxButtons.OK, MessageBoxIcon.Information );
            } ) );

            engine.SetFunction( "print", new Action<string>( o => {
                OutputText( o );
            } ) );

            engine.SetFunction( "assert", new Func<object, object, bool>( ( o1, o2 ) => {

                var currentLine = ( CurrentExecutionLine + 1 ).ToString( );

                if ( ( o1 == null && o2 == null ) ) {
                    OutputText( "Jint Assert succeed at line " + currentLine );
                    return true;
                }

                if ( o1 != null ) {
                    if ( o1.Equals( o2 ) ) {
                        OutputText( "Jint Assert succeed at line " + currentLine );
                        return true;
                    }
                }

                OutputText( "Jint Assert failed at line " + currentLine );

                return false;
            } ) );

            engine.SetFunction( "debugger", new Action( ( ) => {
                if ( engineFlow == eEngineFlow.Breakpointing || engineFlow == eEngineFlow.Stepping ) {
                    engineStatus = eEngineExec.Step;
                }
            } ) );

        }

        void OutputText( string msg ) {
            txtOutput.AppendText( msg + Environment.NewLine );
        }

        void txtScript_LineRemoved( object sender, LineRemovedEventArgs e ) {
            //remove lines from breakpoints
            foreach ( int id in e.RemovedLineUniqueIds )
                if ( BreakpointsLineId.Contains( id ) ) {
                    BreakpointsLineId.Remove( id );
                    Breakpoints.Remove( id );
                }
        }

        void txtScript_PaintLine( object sender, PaintLineEventArgs e ) {
            if ( BreakpointsLineId.Contains( txtScript[ e.LineIndex ].UniqueId ) ) {
                e.Graphics.FillEllipse( new LinearGradientBrush( new Rectangle( 0, e.LineRect.Top, 15, 15 ), Color.Pink, Color.DarkRed, 45 ), 0, e.LineRect.Top, 15, 15 );
                e.Graphics.DrawEllipse( Pens.DarkRed, 0, e.LineRect.Top, 15, 15 );
            }
        }

        void txtScript_TextChanged( object sender, TextChangedEventArgs e ) {

            //clear style of changed range
            e.ChangedRange.ClearStyle( CommentStyle );
            e.ChangedRange.ClearStyle( KeywordsStyle );
            e.ChangedRange.ClearStyle( FunctionsStyle );

            //comment highlighting
            e.ChangedRange.SetStyle( CommentStyle, @"//.*$", RegexOptions.Multiline );
            e.ChangedRange.SetStyle( KeywordsStyle, @"\b(alert|write|break|continue|do|for|import|new|this|void|case|default|else|function|in|return|typeof|while|comment|delete|export|if|label|switch|var|with)\b" );
            e.ChangedRange.SetStyle( FunctionsStyle, @"\b(App|Model)\b" );
            e.ChangedRange.SetStyle( TextStyle, @""".*""" );
            e.ChangedRange.SetStyle( NumberStyle, @"[0-9]+(.[0-9]+)?" );

            //clear folding markers of changed range
            e.ChangedRange.ClearFoldingMarkers( );
            //set folding markers
            e.ChangedRange.SetFoldingMarkers( "{", "}" );
        }

        void txtScript_KeyUp( object sender, KeyEventArgs e ) {
            if ( e.KeyCode == Keys.F5 ) {
                Run( );
                e.Handled = true;
            }
        }

        private void btnNew_Click( object sender, EventArgs e ) {
            txtScript.Text = "";
            ClearWindows( );
        }

        private void btnLoad_Click( object sender, EventArgs e ) {
            var fod = new OpenFileDialog( );
            fod.AddExtension = true;
            fod.AutoUpgradeEnabled = true;
            fod.CheckFileExists = true;
            fod.DefaultExt = "js";
            fod.Filter = "JavaScript|*.js|All Files (*.*)|*.*";
            fod.Multiselect = false;
            fod.Title = "Open JavaScript File";
            if ( fod.ShowDialog( ) == System.Windows.Forms.DialogResult.OK ) {
                txtScript.Text = System.IO.File.ReadAllText( fod.FileName );
                ClearWindows( );
            }
        }

        private void btnSave_Click( object sender, EventArgs e ) {
            var fsd = new SaveFileDialog( );
            fsd.AddExtension = true;
            fsd.AutoUpgradeEnabled = true;
            fsd.DefaultExt = "js";
            fsd.Filter = "JavaScript|*.js|All Files (*.*)|*.*";
            fsd.OverwritePrompt = true;
            fsd.Title = "Save JavaScript File";
            if ( fsd.ShowDialog( ) == System.Windows.Forms.DialogResult.OK ) {
                System.IO.File.WriteAllText( fsd.FileName, txtScript.Text );
            }
        }

        private void btnRun_Click( object sender, EventArgs e ) {
            engine.SetDebugMode( false );
            engineFlow = eEngineFlow.Running;
            Run( );
        }

        private void btnDebug_Click( object sender, EventArgs e ) {

            if ( engineFlow == eEngineFlow.Stopped ) {
                engine.SetDebugMode( true );
                engineFlow = eEngineFlow.Breakpointing;
                engineStatus = eEngineExec.Breakpoint;
                Run( );
            }
            else {
                engineFlow = eEngineFlow.Breakpointing;
                engineStatus = eEngineExec.Continue;
            }
        }

        private void btnStepIn_Click( object sender, EventArgs e ) {
            if ( engineFlow == eEngineFlow.Stopped ) {
                engineStatus = eEngineExec.Step;
                engineFlow = eEngineFlow.Stepping;
                engine.SetDebugMode( true );
                Run( );
            }
            else {
                engineStatus = eEngineExec.Continue;
                engineFlow = eEngineFlow.Stepping;
            }
        }

        private void btnStop_Click( object sender, EventArgs e ) {
            engineStatus = eEngineExec.Stop;
            engineFlow = eEngineFlow.Stopped;
            SetUpEngine( );
        }

        private void tsbToggleBreakpoint_Click( object sender, EventArgs e ) {
            int id = txtScript[ txtScript.Selection.Start.iLine ].UniqueId;
            if ( BreakpointsLineId.Contains( id ) ) {
                //remove Breakpoint
                Breakpoints.Remove( id );
                BreakpointsLineId.Remove( id );
            }
            else {
                //add Breakpoint
                Breakpoints.Add( id );
                BreakpointsLineId.Add( id );
            }

            SetupBreakpoints( );

            //repaint
            txtScript.Invalidate( );
        }

        private void tsbClearBreakpoints_Click( object sender, EventArgs e ) {
            Breakpoints.Clear( );
            BreakpointsLineId.Clear( );
            SetupBreakpoints( );
            txtScript.Invalidate( );
        }

        void SetupBreakpoints( ) {
            engine.BreakPoints.Clear( );
            for ( int i = 0; i < Breakpoints.Count; i++ ) {
                var line = txtScript[ Breakpoints[ i ] ];

                engine.BreakPoints.Add( new Jint.Debugger.BreakPoint( Breakpoints[ i ] + 1, line.Count / 2 ) );
            }
        }

        private void tsbClearOutput_Click( object sender, EventArgs e ) {
            txtOutput.Text = "";
        }

        void ClearWindows( ) {
            txtLocals.Text = "";
            txtOutput.Text = "";
            txtStack.Text = "";
        }

        void Run( ) {
            try {
                ClearWindows( );
                var currentSel = txtScript.Selection;
                if ( txtScript.SelectedText != "" ) {
                    engine.Run( txtScript.SelectedText );
                }
                else {
                    engine.Run( txtScript.Text );
                }
                CurrentExecutionLine = -1;
                engineFlow = eEngineFlow.Stopped;
                txtScript.Selection = currentSel;
                txtScript.DoSelectionVisible( );
                txtScript.Invalidate( );
            }
            catch ( Exception ex ) {
                if ( !ex.Message.Contains( "StopEngineException" ) ) {
                    MessageBox.Show( ex.Message, "Script Error", MessageBoxButtons.OK, MessageBoxIcon.Exclamation );
                }
                engineStatus = eEngineExec.Stop;
                engineFlow = eEngineFlow.Stopped;
                SetUpEngine( );
            }
        }

        void engine_Step( object sender, Jint.Debugger.DebugInformation e ) {

            CurrentExecutionLine = e.CurrentStatement.Source.Start.Line - 1;
            txtScript.Selection.Start = new Place( 0, CurrentExecutionLine );
            txtScript.Selection.End = new Place( txtScript[ CurrentExecutionLine ].Count, CurrentExecutionLine );
            txtScript.DoSelectionVisible( );
            txtScript.Invalidate( );

            ShowStack( e.CallStack, e.CurrentStatement.Source.Code );
            ShowLocals( e.Locals );
            Application.DoEvents( );
            Application.DoEvents( );

            while ( ( engineStatus == eEngineExec.Step ) ||
                ( ( engineStatus == eEngineExec.Breakpoint )
                    && BreakpointsLineId.Contains( CurrentExecutionLine ) ) ) {
                Application.DoEvents( );
            }

            if ( engineStatus == eEngineExec.Continue ) {
                if ( engineFlow == eEngineFlow.Stepping ) {
                    engineStatus = eEngineExec.Step;
                }
                else if ( engineFlow == eEngineFlow.Breakpointing ) {
                    engineStatus = eEngineExec.Breakpoint;
                }
            }

            if ( engineStatus == eEngineExec.Stop )
                throw new StopEngineException( );

        }

        void ShowLocals( Jint.Native.JsDictionaryObject locals ) {
            var sb = new StringBuilder( );
            foreach ( var name in locals.GetKeys( ) ) {
                if ( locals[ name ].Class == "Function" )
                    continue;
                if ( locals[ name ].Value != null ) {
                    sb.AppendLine( name + ": " + locals[ name ].Value.ToString( ) );
                }
                else {
                    sb.AppendLine( name + ": null" );
                }
            }
            txtLocals.Text = sb.ToString( );
        }

        void ShowStack( Stack<string> stack, string Current ) {
            var sb = new StringBuilder( );
            sb.AppendLine( Current );
            var st = stack.ToArray( );
            for ( int i = 0; i < st.Length; i++ ) {
                sb.AppendLine( st[ i ] );
            }
            txtStack.Text = sb.ToString( );
        }


        class StopEngineException: Exception { }
    }
}
